In [97]:
import pandas as pd
import xgboost as xgb
import numpy as np
from sklearn.metrics import roc_auc_score, roc_curve
import plotly.graph_objects as go
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
In [98]:
np.random.seed(42)
In [99]:
import plotly.io as pio
pio.renderers.default = "notebook"

GINI¶

Miara Giniego jest szeroko stosowana w scoringu kredytowym i analizie ryzyka. Choć często używana jako pojedyncza liczba, jej interpretacja i sens statystyczny zależą silnie od liczby obserwacji, w szczególności liczby defaultów.

0. Związek Giniego z ROC/AUC¶

$\text{Gini} = 2 \cdot \text{AUC} - 1$

1. Jak działa Gini / AUC w Pythonie¶

In [100]:
# y = 1 -> default (bad), y = 0 -> good
y_true = np.array([1, 1, 0, 0])
y_score = np.array([0.9, 0.8, 0.7, 0.1])

auc = roc_auc_score(y_true, y_score)
gini = 2 * auc - 1

auc, gini
Out[100]:
(1.0, 1.0)

1️⃣ Perspektywa statystyczna – Mann–Whitney U $$AUC=\frac{U}{n_{bad}⋅n_{good}}=P(score_{bad}>score_{good})+0.5⋅P(score_{bad}=score_{good})$$

  • Liczymy wszystkie pary (bad, good)
  • Każda para daje: 1 jeśli bad > good, 0.5 jeśli remis, 0 jeśli bad < good
  • Wynik / liczba par = AUC

To jest bezpośrednie podejście statystyczne, równoważne testowi Mann–Whitneya.

W takim razie w poprzednim przykładzie możemy AUC interpretować następująco:

Bad scores: [0.9, 0.8] Good scores: [0.7, 0.1]

Porównujemy wszystkie pary:

bad good wynik
0.9 0.7 bad > good
0.9 0.1 bad > good
0.8 0.7 bad > good
0.8 0.1 bad > good

➡️ 4/4 sukcesy ➡️ AUC = 1 ➡️ Gini = 1

In [101]:
#funkcja licząca manualnie wartość GINI z powyższego wzoru na AUC
def manual_auc(y_true, y_score):
    bad_scores = y_score[y_true == 1]
    good_scores = y_score[y_true == 0]

    total = 0
    count = 0

    for b in bad_scores:
        for g in good_scores:
            if b > g:
                total += 1
            elif b == g:
                total += 0.5
            count += 1

    return total / count

manual_auc(y_true, y_score)
Out[101]:
1.0

2️⃣ Perspektywa geometryczna – pole pod krzywą ROC

Budujemy krzywą ROC: TPR vs FPR dla kolejnych progów score

Liczymy pole pod krzywą (trapezoidalna reguła numeryczna)

Wynik jest dokładnie tym samym co w punkcie 1, ale nie wymaga jawnego liczenia par ani statystyki U

ROC (Receiver Operating Characteristic) to wykres:¶

oś X: FPR (False Positive Rate) = FP / (FP + TN)
oś Y: TPR (True Positive Rate) = TP / (TP + FN)

Każdy punkt na krzywej odpowiada innemu progowi decyzyjnemu, który zamienia prawdopodobieństwa / score na klasy 0/1.

  • TPR (czułość / sensitivity): jaki odsetek rzeczywiście pozytywnych przypadków model poprawnie wykrył - w przypadku banku przypadek pozytywny to bad, czyli ta definicja powinna brzmieć: odsetek dobrze zaklasyfikowanych bad.
  • FPR (1 – specyficzność): jaki odsetek negatywnych przypadków model błędnie uznał za pozytywne - w przypadku banku przypadek pozytywny to bad czyli ta definicja powinna brzmieć: odsetek good błędnie zaklasyfikowanych jako bad.

Idealny klasyfikator – lewy górny róg (0, 1)¶

Lewy górny róg wykresu ROC to punkt:

  • FPR = 0 → brak fałszywych alarmów
  • TPR = 1 → wykryliśmy wszystkie pozytywne przypadki

Czyli: „Model wykrywa wszystko, co powinien, i nie myli się ani razu”. To jest ideał, do którego dążymy. Chcemy jednocześnie: wysokiej czułości (TPR ↑), aby nie przegapić pozytywnych przypadków oraz niskiego FPR (FPR ↓), aby nie generować fałszywych alarmów. Krzywa ROC pokazuje wszystkie możliwe progi decyzyjne: każdy punkt to inny kompromis między TPR i FPR. Dobra krzywa ROC oznacza, że istnieje próg, dla którego TPR jest wysoki przy czym FPR jest niski.Im bardziej krzywa „odgina się” w stronę lewego górnego rogu, tym lepiej model oddziela klasy.

  • Przekątna (linia losowa) - krzywa ROC blisko przekątnej (od (0,0) do (1,1)), wtedy TPR ≈ FPR, czyli model działa jak losowanie, AUC ≈ 0.5.
  • Prawy górny róg - TPR wysoki, ale FPR też wysoki, czyli model „łapie wszystko”, ale kosztem wielu fałszywych alarmów np. spam filtr, który oznacza prawie każdy mail jako spam
  • Lewy dolny róg - FPR niski, ale TPR też niski, czyli model jest bardzo ostrożny, ale prawie nic nie wykrywa.

Jak rysuje się krzywą ROC¶

Ogólna metoda:

  1. Weź wszystkie unikalne wartości score (od największego do najmniejszego) jako progi.
  2. Dla każdego progu:
    • przewiduj 1 jeśli score ≥ próg, inaczej 0
    • policz TP, FP, TN, FN
    • oblicz TPR = TP / (TP + FN)
    • oblicz FPR = FP / (FP + TN)
  3. Umieść punkt (FPR, TPR) na wykresie
  4. Połącz punkty linią → to jest krzywa ROC
In [102]:
#funkcja do rysowania macierzy pomyłek
def print_c_m(thr, y_score, y_true, tpr, fpr):

    print(f'cut-off: {thr}')
    print(f'predykcje: {y_score}')
    print(f'predykcje z obcięciem do cutoff: {(y_score >= thr).astype(int)}')
    print(f'wartość prawdziwa: {y_true}')
    print(f'Ture Positive Rate: {tpr}')
    print(f'False Positive Rate: {fpr}')

    cm = confusion_matrix(y_true, y_pred)

    disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['0 - negative', '1 - positive'])
    disp.plot()
    return plt.show()
In [103]:
# Przykładowe dane
y_true = np.array([1, 1, 0, 1, 0, 1, 0, 0, 1])
y_score = np.array([0.9, 0.8, 0.7, 0.7, 0.1, 0.65, 0.2, 0.3, 0.85])

# Unikalne score'y jako progi - sortowanie od największy SCORE do najmniejszy SCORE
thresholds = np.sort(np.unique(y_score))[::-1]

tpr_list = []
fpr_list = []

for thr in thresholds:
    y_pred = (y_score >= thr).astype(int)
    TP = np.sum((y_true == 1) & (y_pred == 1))
    FP = np.sum((y_true == 0) & (y_pred == 1))
    FN = np.sum((y_true == 1) & (y_pred == 0))
    TN = np.sum((y_true == 0) & (y_pred == 0))
    
    TPR = TP / (TP + FN)
    FPR = FP / (FP + TN)
    
    tpr_list.append(TPR)
    fpr_list.append(FPR)

    print(f'TP: {TP}, FP: {FP}, FN: {FN}, TN: {TN}')
    print_c_m(thr, y_score, y_true, TPR, FPR)
    
# Dodaj punkt (0,0) i (1,1) żeby zamknąć krzywą
tpr_list = [0] + tpr_list + [1]
fpr_list = [0] + fpr_list + [1]

# Wykres ROC
plt.figure(figsize=(6,6))
plt.plot(fpr_list, tpr_list, marker='o', color='blue', label='ROC curve')
plt.plot([0,1], [0,1], color='grey', linestyle='--', label='Random')
plt.xlabel('False Positive Rate (1- Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('ROC curve - manual calculation')
plt.grid(True)
plt.legend()
plt.show()
TP: 1, FP: 0, FN: 4, TN: 4
cut-off: 0.9
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 0 0 0 0 0 0 0 0]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 0.2
False Positive Rate: 0.0
No description has been provided for this image
TP: 2, FP: 0, FN: 3, TN: 4
cut-off: 0.85
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 0 0 0 0 0 0 0 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 0.4
False Positive Rate: 0.0
No description has been provided for this image
TP: 3, FP: 0, FN: 2, TN: 4
cut-off: 0.8
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 1 0 0 0 0 0 0 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 0.6
False Positive Rate: 0.0
No description has been provided for this image
TP: 4, FP: 1, FN: 1, TN: 3
cut-off: 0.7
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 1 1 1 0 0 0 0 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 0.8
False Positive Rate: 0.25
No description has been provided for this image
TP: 5, FP: 1, FN: 0, TN: 3
cut-off: 0.65
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 1 1 1 0 1 0 0 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 1.0
False Positive Rate: 0.25
No description has been provided for this image
TP: 5, FP: 2, FN: 0, TN: 2
cut-off: 0.3
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 1 1 1 0 1 0 1 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 1.0
False Positive Rate: 0.5
No description has been provided for this image
TP: 5, FP: 3, FN: 0, TN: 1
cut-off: 0.2
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 1 1 1 0 1 1 1 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 1.0
False Positive Rate: 0.75
No description has been provided for this image
TP: 5, FP: 4, FN: 0, TN: 0
cut-off: 0.1
predykcje: [0.9  0.8  0.7  0.7  0.1  0.65 0.2  0.3  0.85]
predykcje z obcięciem do cutoff: [1 1 1 1 1 1 1 1 1]
wartość prawdziwa: [1 1 0 1 0 1 0 0 1]
Ture Positive Rate: 1.0
False Positive Rate: 1.0
No description has been provided for this image
No description has been provided for this image

✅ Kluczowe

  • Obie metody dały dokładnie tę samą wartość AUC
  • W scikit‑learn roc_auc_score używa drugiego sposobu (krzywa ROC + pole trapezoidów), ale wynik jest matematycznie równoważny U-statystyce Mann–Whitneya
  • Różnica jest tylko w sposobie obliczeń, nie w wyniku.

Symulacje wartości GINI oraz błędu (odchylenia standardowego) dla różnych wielkości zbioru BAD¶

Założenia:

  • dla stałego ODR (na poziomie 2%) generowane są zestawy wartości BAD oraz GOOD, przy czym wartości BAD generowane są od 1 do 800 co 5 i do tego dobierane są wartości GOOD aby zachować proporcję ODR,
  • dla każdej pary BAD-GOOD symulowane są wartości SCORE, które mogą przyjmować z rozkładu normalnego,
  • SCORE dla obserwacji BAD jest z rozkładu normalnego N(signal, 1). Wartość signal czyli średniej powinna być ustawiona np. na -2 tak, aby rokład obserwacji BAD był przesunięty w lewą stronę.
  • SCORE dla obserwacji GOOD jest z rokładu normalnego N(0,1).
In [104]:
#przykładowy wygenerowany rozkład SCORE dla liczby BAD = 200 i liczby GOOD = 800
bad_scores = np.random.normal(loc=-2, scale=1, size=200)
good_scores = np.random.normal(loc=0, scale=1, size=800)

fig = go.Figure()
# Histogram bad
fig.add_trace(go.Histogram(x=bad_scores, nbinsx=15, name='Bad (default)', opacity=0.6, marker_color='red'))
# Histogram good
fig.add_trace(go.Histogram(x=good_scores, nbinsx=15, name='Good', opacity=0.6, marker_color='blue'))

fig.update_layout(
    title='Rozkład score dla Good i Bad',
    xaxis_title='Score',
    yaxis_title='Liczba obserwacji',
    barmode='overlay',  # nakładanie się histogramów
    template='plotly_white')
fig.show()
In [105]:
#funkcja do symulowania wartości SCORE dla określonej liczby GOOD i BAD
def simulate_scores(n_bad, n_good, signal=0.5):
    bad_scores = np.random.normal(loc=signal, scale=1, size=n_bad)
    good_scores = np.random.normal(loc=0, scale=1, size=n_good)
    
    y_true = np.concatenate([np.ones(n_bad), np.zeros(n_good)])
    y_score = np.concatenate([bad_scores, good_scores])
    
    return y_true, y_score



def simulate_gini(n_bad, n_good, signal=0.5):

    y_true, y_score = simulate_scores(n_bad, n_good, signal)
    auc = roc_auc_score(y_true, y_score)
    
    return 2 * auc - 1
In [106]:
ODR = 0.02 #2%

#wyliczenie liczby good tak, żeby zachować zawsze ODR=2%
#x/(x+y) = ODR
#x=ODR*(x+y)
#x=ODR*x + ODR*y
#x-ODR*x = ODR*y
#y = x*(1-ODR)/ODR

#gdzie x=x
bads = []
goods = []
gini_val = []
gini_std = []

for b in range(1, 800, 5):
    g = int(round(b*(1-ODR)/ODR, 0))
    bads.append(b)
    goods.append(g)
    print(f'BADS: {b}, GOOD: {g} -> ODR: {round(b/g, 2)}')

    ginis_simulations = [simulate_gini(b, g, signal=-2) for _ in range(100)] #symulujemy wartość SCORE dla GOOD i BAD 100 razy
    gini_val.append(-1*np.mean(ginis_simulations)) #symulowaliśmy wartości SCORE a nie PD - stąd błędny kierunek i wymóg minusa
    gini_std.append(np.std(ginis_simulations))
    
BADS: 1, GOOD: 49 -> ODR: 0.02
BADS: 6, GOOD: 294 -> ODR: 0.02
BADS: 11, GOOD: 539 -> ODR: 0.02
BADS: 16, GOOD: 784 -> ODR: 0.02
BADS: 21, GOOD: 1029 -> ODR: 0.02
BADS: 26, GOOD: 1274 -> ODR: 0.02
BADS: 31, GOOD: 1519 -> ODR: 0.02
BADS: 36, GOOD: 1764 -> ODR: 0.02
BADS: 41, GOOD: 2009 -> ODR: 0.02
BADS: 46, GOOD: 2254 -> ODR: 0.02
BADS: 51, GOOD: 2499 -> ODR: 0.02
BADS: 56, GOOD: 2744 -> ODR: 0.02
BADS: 61, GOOD: 2989 -> ODR: 0.02
BADS: 66, GOOD: 3234 -> ODR: 0.02
BADS: 71, GOOD: 3479 -> ODR: 0.02
BADS: 76, GOOD: 3724 -> ODR: 0.02
BADS: 81, GOOD: 3969 -> ODR: 0.02
BADS: 86, GOOD: 4214 -> ODR: 0.02
BADS: 91, GOOD: 4459 -> ODR: 0.02
BADS: 96, GOOD: 4704 -> ODR: 0.02
BADS: 101, GOOD: 4949 -> ODR: 0.02
BADS: 106, GOOD: 5194 -> ODR: 0.02
BADS: 111, GOOD: 5439 -> ODR: 0.02
BADS: 116, GOOD: 5684 -> ODR: 0.02
BADS: 121, GOOD: 5929 -> ODR: 0.02
BADS: 126, GOOD: 6174 -> ODR: 0.02
BADS: 131, GOOD: 6419 -> ODR: 0.02
BADS: 136, GOOD: 6664 -> ODR: 0.02
BADS: 141, GOOD: 6909 -> ODR: 0.02
BADS: 146, GOOD: 7154 -> ODR: 0.02
BADS: 151, GOOD: 7399 -> ODR: 0.02
BADS: 156, GOOD: 7644 -> ODR: 0.02
BADS: 161, GOOD: 7889 -> ODR: 0.02
BADS: 166, GOOD: 8134 -> ODR: 0.02
BADS: 171, GOOD: 8379 -> ODR: 0.02
BADS: 176, GOOD: 8624 -> ODR: 0.02
BADS: 181, GOOD: 8869 -> ODR: 0.02
BADS: 186, GOOD: 9114 -> ODR: 0.02
BADS: 191, GOOD: 9359 -> ODR: 0.02
BADS: 196, GOOD: 9604 -> ODR: 0.02
BADS: 201, GOOD: 9849 -> ODR: 0.02
BADS: 206, GOOD: 10094 -> ODR: 0.02
BADS: 211, GOOD: 10339 -> ODR: 0.02
BADS: 216, GOOD: 10584 -> ODR: 0.02
BADS: 221, GOOD: 10829 -> ODR: 0.02
BADS: 226, GOOD: 11074 -> ODR: 0.02
BADS: 231, GOOD: 11319 -> ODR: 0.02
BADS: 236, GOOD: 11564 -> ODR: 0.02
BADS: 241, GOOD: 11809 -> ODR: 0.02
BADS: 246, GOOD: 12054 -> ODR: 0.02
BADS: 251, GOOD: 12299 -> ODR: 0.02
BADS: 256, GOOD: 12544 -> ODR: 0.02
BADS: 261, GOOD: 12789 -> ODR: 0.02
BADS: 266, GOOD: 13034 -> ODR: 0.02
BADS: 271, GOOD: 13279 -> ODR: 0.02
BADS: 276, GOOD: 13524 -> ODR: 0.02
BADS: 281, GOOD: 13769 -> ODR: 0.02
BADS: 286, GOOD: 14014 -> ODR: 0.02
BADS: 291, GOOD: 14259 -> ODR: 0.02
BADS: 296, GOOD: 14504 -> ODR: 0.02
BADS: 301, GOOD: 14749 -> ODR: 0.02
BADS: 306, GOOD: 14994 -> ODR: 0.02
BADS: 311, GOOD: 15239 -> ODR: 0.02
BADS: 316, GOOD: 15484 -> ODR: 0.02
BADS: 321, GOOD: 15729 -> ODR: 0.02
BADS: 326, GOOD: 15974 -> ODR: 0.02
BADS: 331, GOOD: 16219 -> ODR: 0.02
BADS: 336, GOOD: 16464 -> ODR: 0.02
BADS: 341, GOOD: 16709 -> ODR: 0.02
BADS: 346, GOOD: 16954 -> ODR: 0.02
BADS: 351, GOOD: 17199 -> ODR: 0.02
BADS: 356, GOOD: 17444 -> ODR: 0.02
BADS: 361, GOOD: 17689 -> ODR: 0.02
BADS: 366, GOOD: 17934 -> ODR: 0.02
BADS: 371, GOOD: 18179 -> ODR: 0.02
BADS: 376, GOOD: 18424 -> ODR: 0.02
BADS: 381, GOOD: 18669 -> ODR: 0.02
BADS: 386, GOOD: 18914 -> ODR: 0.02
BADS: 391, GOOD: 19159 -> ODR: 0.02
BADS: 396, GOOD: 19404 -> ODR: 0.02
BADS: 401, GOOD: 19649 -> ODR: 0.02
BADS: 406, GOOD: 19894 -> ODR: 0.02
BADS: 411, GOOD: 20139 -> ODR: 0.02
BADS: 416, GOOD: 20384 -> ODR: 0.02
BADS: 421, GOOD: 20629 -> ODR: 0.02
BADS: 426, GOOD: 20874 -> ODR: 0.02
BADS: 431, GOOD: 21119 -> ODR: 0.02
BADS: 436, GOOD: 21364 -> ODR: 0.02
BADS: 441, GOOD: 21609 -> ODR: 0.02
BADS: 446, GOOD: 21854 -> ODR: 0.02
BADS: 451, GOOD: 22099 -> ODR: 0.02
BADS: 456, GOOD: 22344 -> ODR: 0.02
BADS: 461, GOOD: 22589 -> ODR: 0.02
BADS: 466, GOOD: 22834 -> ODR: 0.02
BADS: 471, GOOD: 23079 -> ODR: 0.02
BADS: 476, GOOD: 23324 -> ODR: 0.02
BADS: 481, GOOD: 23569 -> ODR: 0.02
BADS: 486, GOOD: 23814 -> ODR: 0.02
BADS: 491, GOOD: 24059 -> ODR: 0.02
BADS: 496, GOOD: 24304 -> ODR: 0.02
BADS: 501, GOOD: 24549 -> ODR: 0.02
BADS: 506, GOOD: 24794 -> ODR: 0.02
BADS: 511, GOOD: 25039 -> ODR: 0.02
BADS: 516, GOOD: 25284 -> ODR: 0.02
BADS: 521, GOOD: 25529 -> ODR: 0.02
BADS: 526, GOOD: 25774 -> ODR: 0.02
BADS: 531, GOOD: 26019 -> ODR: 0.02
BADS: 536, GOOD: 26264 -> ODR: 0.02
BADS: 541, GOOD: 26509 -> ODR: 0.02
BADS: 546, GOOD: 26754 -> ODR: 0.02
BADS: 551, GOOD: 26999 -> ODR: 0.02
BADS: 556, GOOD: 27244 -> ODR: 0.02
BADS: 561, GOOD: 27489 -> ODR: 0.02
BADS: 566, GOOD: 27734 -> ODR: 0.02
BADS: 571, GOOD: 27979 -> ODR: 0.02
BADS: 576, GOOD: 28224 -> ODR: 0.02
BADS: 581, GOOD: 28469 -> ODR: 0.02
BADS: 586, GOOD: 28714 -> ODR: 0.02
BADS: 591, GOOD: 28959 -> ODR: 0.02
BADS: 596, GOOD: 29204 -> ODR: 0.02
BADS: 601, GOOD: 29449 -> ODR: 0.02
BADS: 606, GOOD: 29694 -> ODR: 0.02
BADS: 611, GOOD: 29939 -> ODR: 0.02
BADS: 616, GOOD: 30184 -> ODR: 0.02
BADS: 621, GOOD: 30429 -> ODR: 0.02
BADS: 626, GOOD: 30674 -> ODR: 0.02
BADS: 631, GOOD: 30919 -> ODR: 0.02
BADS: 636, GOOD: 31164 -> ODR: 0.02
BADS: 641, GOOD: 31409 -> ODR: 0.02
BADS: 646, GOOD: 31654 -> ODR: 0.02
BADS: 651, GOOD: 31899 -> ODR: 0.02
BADS: 656, GOOD: 32144 -> ODR: 0.02
BADS: 661, GOOD: 32389 -> ODR: 0.02
BADS: 666, GOOD: 32634 -> ODR: 0.02
BADS: 671, GOOD: 32879 -> ODR: 0.02
BADS: 676, GOOD: 33124 -> ODR: 0.02
BADS: 681, GOOD: 33369 -> ODR: 0.02
BADS: 686, GOOD: 33614 -> ODR: 0.02
BADS: 691, GOOD: 33859 -> ODR: 0.02
BADS: 696, GOOD: 34104 -> ODR: 0.02
BADS: 701, GOOD: 34349 -> ODR: 0.02
BADS: 706, GOOD: 34594 -> ODR: 0.02
BADS: 711, GOOD: 34839 -> ODR: 0.02
BADS: 716, GOOD: 35084 -> ODR: 0.02
BADS: 721, GOOD: 35329 -> ODR: 0.02
BADS: 726, GOOD: 35574 -> ODR: 0.02
BADS: 731, GOOD: 35819 -> ODR: 0.02
BADS: 736, GOOD: 36064 -> ODR: 0.02
BADS: 741, GOOD: 36309 -> ODR: 0.02
BADS: 746, GOOD: 36554 -> ODR: 0.02
BADS: 751, GOOD: 36799 -> ODR: 0.02
BADS: 756, GOOD: 37044 -> ODR: 0.02
BADS: 761, GOOD: 37289 -> ODR: 0.02
BADS: 766, GOOD: 37534 -> ODR: 0.02
BADS: 771, GOOD: 37779 -> ODR: 0.02
BADS: 776, GOOD: 38024 -> ODR: 0.02
BADS: 781, GOOD: 38269 -> ODR: 0.02
BADS: 786, GOOD: 38514 -> ODR: 0.02
BADS: 791, GOOD: 38759 -> ODR: 0.02
BADS: 796, GOOD: 39004 -> ODR: 0.02
In [107]:
#GINI wartość w zależności od liczby BAD
fig = go.Figure()
fig.add_trace(go.Scatter(x=bads, y=gini_val, mode='lines+markers', name='Gini', line=dict(color='blue', width=2), marker=dict(size=8)))
fig.update_layout(
    title='Wpływ liczby defaultów na wartość Giniego',
    xaxis_title='Liczba defaultów',
    yaxis_title='Gini (%)',
    yaxis=dict(tickformat='.1%'),
    template='plotly_white')
fig.show()

#GINI odchylenie standardowe w zależności od liczby BAD
fig = go.Figure()
fig.add_trace(go.Scatter(x=bads, y=gini_std, mode='lines+markers', name='Gini', line=dict(color='red', width=2), marker=dict(size=8)))
fig.update_layout(
    title='Wpływ liczby defaultów na odchylenie standardowe wartości Giniego',
    xaxis_title='Liczba defaultów',
    yaxis_title='STD',
    template='plotly_white')
fig.show()      
In [117]:
n_bad_s = 10
y_true_small, y_score_small = simulate_scores(n_bad=n_bad_s,n_good=400)

auc_s = roc_auc_score(y_true=y_true_small, y_score=y_score_small)
fpr_s, tpr_s, _ = roc_curve(y_true_small, y_score_small)
auc_s = roc_auc_score(y_true_small, y_score_small)
In [118]:
n_bad_l = 100
y_true_large, y_score_large = simulate_scores(n_bad=n_bad_l,n_good=400)

auc_l = roc_auc_score(y_true=y_true_large, y_score=y_score_large)
fpr_l, tpr_l, _ = roc_curve(y_true_large, y_score_large)
auc_l = roc_auc_score(y_true_large, y_score_large)
In [119]:
fig = go.Figure()
# Krzywa ROC
fig.add_trace(go.Scatter(x=fpr_s, y=tpr_s, mode='lines+markers', name=f'ROC (n_bads = {n_bad_s}, AUC = {auc_s:.3f})',
                         line=dict(width=2)))
fig.add_trace(go.Scatter(x=fpr_l, y=tpr_l, mode='lines+markers', name=f'ROC (n_bads = {n_bad_l}, AUC = {auc_l:.3f})',
                         line=dict(width=2)))
# Linia losowa
fig.add_trace(go.Scatter(x=[0, 1], y=[0, 1], mode='lines', name='Random classifier', line=dict(dash='dash', color='gray')))

fig.update_layout(
    title='Krzywa ROC (TPR vs FPR)',
    xaxis_title='False Positive Rate (FPR)',
    yaxis_title='True Positive Rate (TPR)',
    xaxis=dict(range=[0, 1.01]),
    yaxis=dict(range=[0, 1.1]),
    template='plotly_white')
fig.show()
In [ ]:
# Wyliczenie ROC manualnie dla małej liczby 10 BAD - widać lepiej przyczynę schodków
y_true = y_true_small.copy()
y_score = y_score_small.copy()

# Unikalne score'y jako progi - sortowanie od największy SCORE do najmniejszy SCORE
thresholds = np.sort(np.unique(y_score))[::-1]

tpr_list = []
fpr_list = []
licznik_podzialow = 0

for thr in thresholds:
    y_pred = (y_score >= thr).astype(int)
    TP = np.sum((y_true == 1) & (y_pred == 1))
    FP = np.sum((y_true == 0) & (y_pred == 1))
    FN = np.sum((y_true == 1) & (y_pred == 0))
    TN = np.sum((y_true == 0) & (y_pred == 0))
    
    TPR = TP / (TP + FN)
    FPR = FP / (FP + TN)
    
    tpr_list.append(TPR)
    fpr_list.append(FPR)

    licznik_podzialow += 1
    print(f'Podział nr: {licznik_podzialow}')
    print(f'TP: {TP}, FP: {FP}, FN: {FN}, TN: {TN}')
    print(f'TPR: {TPR}, FPR: {FPR}')
    #print_c_m(thr, y_score, y_true, TPR, FPR)
    
# Dodaj punkt (0,0) i (1,1) żeby zamknąć krzywą
tpr_list = [0] + tpr_list + [1]
fpr_list = [0] + fpr_list + [1]

# Wykres ROC
plt.figure(figsize=(6,6))
plt.plot(fpr_list, tpr_list, marker='o', color='blue', label='ROC curve')
plt.plot([0,1], [0,1], color='grey', linestyle='--', label='Random')
plt.xlabel('False Positive Rate (1- Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('ROC curve - manual calculation')
plt.grid(True)
plt.legend()
plt.show()
In [ ]:
# Wyliczenie ROC manualnie dla dużej liczby  BAD - widać lepiej przyczynę schodków
y_true = y_true_large.copy()
y_score = y_score_large.copy()

# Unikalne score'y jako progi - sortowanie od największy SCORE do najmniejszy SCORE
thresholds = np.sort(np.unique(y_score))[::-1]

tpr_list = []
fpr_list = []
licznik_podzialow = 0

for thr in thresholds:
    y_pred = (y_score >= thr).astype(int)
    TP = np.sum((y_true == 1) & (y_pred == 1))
    FP = np.sum((y_true == 0) & (y_pred == 1))
    FN = np.sum((y_true == 1) & (y_pred == 0))
    TN = np.sum((y_true == 0) & (y_pred == 0))
    
    TPR = TP / (TP + FN)
    FPR = FP / (FP + TN)
    
    tpr_list.append(TPR)
    fpr_list.append(FPR)

    licznik_podzialow += 1
    print(f'Podział nr: {licznik_podzialow}')
    print(f'TP: {TP}, FP: {FP}, FN: {FN}, TN: {TN}')
    print(f'TPR: {TPR}, FPR: {FPR}')
    #print_c_m(thr, y_score, y_true, TPR, FPR)
    
# Dodaj punkt (0,0) i (1,1) żeby zamknąć krzywą
tpr_list = [0] + tpr_list + [1]
fpr_list = [0] + fpr_list + [1]

# Wykres ROC
plt.figure(figsize=(6,6))
plt.plot(fpr_list, tpr_list, marker='o', color='blue', label='ROC curve')
plt.plot([0,1], [0,1], color='grey', linestyle='--', label='Random')
plt.xlabel('False Positive Rate (1- Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('ROC curve - manual calculation')
plt.grid(True)
plt.legend()
plt.show()

Wartośc GINI w porównaniu do rozkładu GOOD BAD¶

In [113]:
#funkcja do wyliczania rozkładów gęstości dla good i bad
from scipy.stats import gaussian_kde


def kde_from_scores(
    y_true,
    y_score,
    good_value=0,
    bad_value=1,
    n_points=500,
    bandwidth=None
):
    """
    Liczy KDE (PDF) score osobno dla good i bad.

    Parametry
    ----------
    y_true : array-like
        Wartości rzeczywiste (0/1)
    y_score : array-like
        Score / predykcja modelu
    good_value : int
        Wartość oznaczająca klasę 'good'
    bad_value : int
        Wartość oznaczająca klasę 'bad'
    n_points : int
        Liczba punktów siatki X
    bandwidth : float lub None
        Bandwidth dla KDE (None = automatyczny)

    Zwraca
    -------
    x : np.ndarray
        Wspólna oś score
    pdf_good : np.ndarray
        Gęstość dla good
    pdf_bad : np.ndarray
        Gęstość dla bad
    """

    y_true = np.asarray(y_true)
    y_score = np.asarray(y_score)

    good_scores = y_score[y_true == good_value]
    bad_scores  = y_score[y_true == bad_value]

    if len(good_scores) == 0 or len(bad_scores) == 0:
        raise ValueError("Brak obserwacji good lub bad")

    x_min = min(good_scores.min(), bad_scores.min())
    x_max = max(good_scores.max(), bad_scores.max())
    x = np.linspace(x_min, x_max, n_points)

    kde_good = gaussian_kde(good_scores, bw_method=bandwidth)
    kde_bad  = gaussian_kde(bad_scores, bw_method=bandwidth)

    pdf_good = kde_good(x)
    pdf_bad  = kde_bad(x)

    return x, pdf_good, pdf_bad
In [114]:
#funkcja do rysowania rozkładów gęstości
def check_gini_vs_good_bad(n_bad, n_good, signal):
    y_true, y_score = simulate_scores(n_bad=n_bad, n_good=n_good, signal=signal)
    gini_val = -(2 * roc_auc_score(y_true=y_true, y_score=y_score) - 1)
    x, pdf_good, pdf_bad = kde_from_scores(y_true=y_true, y_score=y_score, good_value=0, bad_value=1, n_points=400)

    fig = go.Figure()
    fig.add_trace(go.Scatter(x=x, y=pdf_good, mode='lines+markers', name='Good', line=dict(width=2)))
    fig.add_trace(go.Scatter(x=x, y=pdf_bad, mode='lines+markers', name='Bad (default)', line=dict(width=2)))

    fig.update_layout(
        title=f'Rozkład score dla Good (n={n_good} z N(0,1)) i Bad (n={n_bad} z N({signal},1)). GINI: {gini_val:.2%}',
        xaxis_title='Score',
        yaxis_title='Liczba obserwacji',
        template='plotly_white')
    fig.show()

    fig = go.Figure()
In [115]:
check_gini_vs_good_bad(400, 2000, 0)
check_gini_vs_good_bad(400, 2000, -0.5)
check_gini_vs_good_bad(400, 2000, -1)
check_gini_vs_good_bad(400, 2000, -1.5)
check_gini_vs_good_bad(400, 2000, -2)